Intro

"객체는 처리의 추상화다. 스레드는 일정의 추상화다."
- 제임스 O.코플리엔

동시성과 깔끔한 코드는 양립하기 어렵다.
스레드를 하나만 실행하는 코드는 짜기가 쉽다.
간략히 알아보자.

동시성이 필요한 이유?

동시성은 결합(coupling) 을 없애는 전략이다.
즉, 무엇(what)언제(when) 를 분리하는 전략이다.
그래서 디버깅을 할 때 **정지점(breakpoint)**을 정한 후 어느 정지점에 걸렸는지 살펴본다.

서블릿은 EJB컨테이너 안에서 돌아가는데, 이들은 동시성을 부분적으로 관리한다.
각 서블릿 스레드는 다른 서블릿과 무관하게 돌아간다.
서블릿 모델이 제공하는 구조적 이점이 아주 크다.

하지만 작업 처리량(throughput) 개선이라는 요구로 인해 직접적인 동시성을 구현해야 한다.

미신과 오해

  1. 동시성은 향상 성능을 높여준다.
    • 동시성은 때로 성능을 높여준다.
  2. 동시성을 구현해도 설계는 변하지 않는다.
    • 무엇언제 를 분리하면 시스템 구조가 크게 달라진다.
  3. 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다.
    • 어떻게 동작하는지, 문제가 생기는 지 알아야 한다.

동시성과 관련된 타당한 생각

  1. 동시성은 다소 부하를 유발한다.
  2. 동시성은 복잡하다.
  3. 일반적으로 동시성 버그는 재현하기 어렵다.
  4. 동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다.

난관

public class X {
    private int latsIdUsed;

    public int getNextId() {
        return ++lastIdUsed;
    }
}

인스턴스 X를 생성하고, lastIdUsed 필드를 42로 설정한 다음,
두 스레드가 해당 인스턴스를 공유한다.
두 스레드가 getNextId()를 호출한다고 가정하자.

43, 44 -> 44  
44, 43 -> 44  
43, 43 -> 43  

결과는 셋 중 하나다.

동시성 방어 원칙

단일 책임 원칙(Single Responsibility Principle, SRP)

동시성 관련 코드는 다른 코드와 분리해야 한다.

따름 정리(corollary): 자료 범위를 제한하라

공유 객체를 사용하는 코드 내 임게영역synchronized 키워드로 보호하라.
자료를 캡슐화하라.
공유 자료를 최대한 줄여라.

따름 정리: 자료 사본을 사용하라

어떤 경우엔 각 스레드가 객체를 복사해 사용한 후
한 스레드가 해당 사본에서 결과를 가져와야 한다.
비용은 사본 생성과 가비지 컬렉션에 드는 부하를 상쇄할 가능성이 크다.

따름정리: 스레드는 가능한 독립적으로 구현하라

독립적인 스레드로, 가능하면 다른 프로세서에서, 돌려도 괜찮도록
자료를 독립적인 단위로 분할하라.

라이브러리를 이해하라

자바 5에서 스레드 코드를 구현할 때 좋은 방법은 다음과 같다.

  • 컬렉션 사용
  • 서로 무관한 작업을 수행할 땐 executor 프레임워크 사용
  • 스레드가 차단(blocking) 되지 않는 방법 사용
  • 일부 클래스 라이브러리는 스레드에 안전하지 못함

스레드 환경에 안전한 컬렉션

java.util.concurrent 패키지
ConcurrentHasMap이 HashMap보다 빠르다.
자바에서는 java.util.concurrent, java.util.concurrent.atomic,
java.util.concurrent.locks를 익혀라.

실행 모델을 이해하라

생산자-소비자

생산자 스레드와 소비자 스레드가 사용하는 대기열은 한정된 자원 이다.
빈 공간이 생길 때까지 기다리고, 정보가 채워질 때까지 기다린다.
따라서 둘 다 진행 가능함에도 동시에 기다릴 가능성이 있다.

읽기-쓰기

처리율을 강조하면 기아현상이 생기거나 오래된 정보가 쌓인다.
따라서 읽기 스레드의 요구와 쓰기 스레드의 요구를 적절히 만족시켜
처리율도 적당히 높이고 기아도 방지하는 해법이 필요하다.

식사하는 철학자들

흔한 예시다.
데드락, 라이브락, 처리율 저하, 요율성 저하 등을 공부하고 해법을 직접 구현해보자.

동기화하는 메서드 사이에 존재하는 의존성을 이해하라

공유 클래스 하나에 동기화된 매서드가 여럿이라면 구현이 올바른지 확인하자.

공유 객체 하나에는 메서드 하나만 사용하라.

그게 아니라면 다음을 고려하자.

  • 클라이언트에서 잠금: 첫 번째 메서드 ~ 마지막 메서드까지 잠금 유지
  • 서버에서 잠금: 서버를 잠그고 모든 메서드 호출 후 잠금 해제하는 매서드 구현
  • 연결(Adapted) 서버: 중간 단계 생성, 원래 서버 변경 x

동기화하는 부분을 작게 만들어라

synchronized 키워드를 사용하면 락을 설정한다.
그러므로 남발하는 코드를 작성하지 말자.
반면, 임계영역은 반드시 보호해야 한다.
따라서 임계영역 수를 최대한 줄여야 한다.

동기화 하는 부분을 최대한 작게 만들어라.

올바른 종료 코드는 구현하기 어렵다

데드락은 가장 흔히 발생한다.
오지 않을 시그널을 기다리는 것이다.
종료 코드를 개발 초기부터 고민하고 동작하게 초기부터 구현하라.
생각보다 오래 걸린다.
생각보다 어려우므로 이미 나온 알고리즘을 검토하라.

스레드 코드 테스트하기

문제를 노출하는 테스트 케이스를 작성하라.
프로그램 설정과 시스템 설정과 부하를 바꿔가며 자주 돌려라.
테스트가 실패하면 원인을 추적하라.
다시 돌렸더니 통과하더라는 이유로 그냥 넘어가면 절대로 안 된다.

  • 말이 안 되는 실패는 잠정적인 스레드 문제로 취급

시스템 실패를 ‘일회성’이라 치부하지 마라

  • 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자.

먼저 스레드 환경 밖에서 코드를 올바로 돌려라.

  • 다중 스레드를 쓰는 코드 부분을 다양한 환경에 사용할 수 있도록 스레드 코드 구현
  • 다중 스레드를 쓰는 코드 부분을 상황에 맞춰 조정할 수 있게 작성
  • 프로세서 수보다 많은 스레드를 돌려봐.

스와핑할 때도 문제가 발생할 수 있음
데드락 코드 찾기 쉬워짐

  • 다른 플랫폼에서 돌려보라
  • 코드에 보조 코드를 넣어 돌려라. 강제로 실패를 일으키게 해보라.

직접 구현: wait()/sleep()/yield()/priority()
자동화: AOF(Aspect-Oriented Framework), CGLIB, ASM

결론

  1. SRP 준수
  2. 동시성 오류 원인 이해
  3. 라이브러리와 기본 알고리즘 이해
  4. 보호할 코드 영역을 찾아내는 방법과 특정 코드 영역을 잠그는 방법 이해
  5. 문제를 일회성으로 치부하지 말 것
  6. 보조코드 활용